/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.dq.core.service.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.unidata.mdm.core.service.UPathService;
import org.unidata.mdm.core.type.data.ArrayAttribute;
import org.unidata.mdm.core.type.data.Attribute;
import org.unidata.mdm.core.type.data.ComplexAttribute;
import org.unidata.mdm.core.type.data.DataRecord;
import org.unidata.mdm.core.type.data.SimpleAttribute;
import org.unidata.mdm.core.type.data.impl.AbstractArrayAttribute;
import org.unidata.mdm.core.type.data.impl.AbstractSimpleAttribute;
import org.unidata.mdm.core.type.data.impl.ComplexAttributeImpl;
import org.unidata.mdm.core.type.model.CustomPropertyElement;
import org.unidata.mdm.core.type.upath.UPath;
import org.unidata.mdm.core.type.upath.UPathApplicationMode;
import org.unidata.mdm.core.type.upath.UPathElement;
import org.unidata.mdm.core.type.upath.UPathExecutionContext;
import org.unidata.mdm.core.type.upath.UPathIncompletePath;
import org.unidata.mdm.core.type.upath.UPathResult;
import org.unidata.mdm.dq.core.context.CleanseFunctionContext;
import org.unidata.mdm.dq.core.context.DataQualityContext;
import org.unidata.mdm.dq.core.dto.CleanseFunctionResult;
import org.unidata.mdm.dq.core.dto.DataQualityResult;
import org.unidata.mdm.dq.core.dto.DataQualityResult.RuleExecutionResult;
import org.unidata.mdm.dq.core.service.DataQualityService;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionInputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionOutputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionParam;
import org.unidata.mdm.dq.core.type.io.DataQualityError;
import org.unidata.mdm.dq.core.type.io.DataQualityOutput;
import org.unidata.mdm.dq.core.type.io.DataQualityState;
import org.unidata.mdm.dq.core.type.model.instance.CleanseFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.InputMappingElement;
import org.unidata.mdm.dq.core.type.model.instance.MappingSetElement;
import org.unidata.mdm.dq.core.type.model.instance.OutputMappingElement;
import org.unidata.mdm.dq.core.type.model.instance.PortElement;
import org.unidata.mdm.dq.core.type.model.instance.QualityRuleElement;
import org.unidata.mdm.dq.core.type.model.instance.RuleMappingElement;
import org.unidata.mdm.dq.core.type.model.instance.ValidatingRuleElement;
import org.unidata.mdm.dq.core.type.rule.QualityRuleRunCondition;
import org.unidata.mdm.dq.core.type.rule.SeverityIndicator;
import org.unidata.mdm.dq.core.util.DQUtils;
import org.unidata.mdm.system.service.TextService;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;

/**
 * The Class DataQualityServiceImpl.
 */
@Service
public class DataQualityServiceImpl implements DataQualityService {
    /**
     * Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DataQualityServiceImpl.class);
    /**
     * Empty cycles array.
     */
    private static final RuleExecutionCycle[] EMPTY_CYCLES_ARRAY = new RuleExecutionCycle[0];
    /**
     * UPath service.
     */
    @Autowired
    private UPathService upathService;
    /**
     * The TS.
     */
    @Autowired
    private TextService textService;
    /**
     * {@inheritDoc}
     */
    @Override
    public DataQualityResult apply(DataQualityContext ctx) {

        DataQualityResult result = new DataQualityResult();
        List<MappingSetElement> sets = ctx.getMappings();

        // Nothing to check/enrich. Return.
        if (CollectionUtils.isEmpty(sets) || ctx.getInput().isEmpty()) {
            return result;
        }

        MeasurementPoint.start();
        try {

            // Init output
            DataQualityOutput output = ctx.getInput().toOutput();

            // For each supplied mapping set
            Iterator<MappingSetElement> i = sets.iterator();
            while (i.hasNext()) {

                MappingSetElement set = i.next();

                // For each rule mapping in the set
                for (RuleMappingElement mapping : set) {

                    RuleExecutionResult state = new RuleExecutionResult(set, mapping.getRule());
                    try {

                        // Check, rule is turned off
                        if (skipRule(mapping, ctx)) {
                            state.skip();
                            result.add(state);
                            continue;
                        }

                        // Run rule and collect state
                        runRule(mapping, state, ctx, output);

                    } catch (Exception e) {
                        handleException(state, mapping, e);
                    }

                    result.add(state);
                }
            }

            result.setOutput(output);
            result.setPayload(ctx.getPayload());

            return result;
        } finally {
            MeasurementPoint.stop();
        }
    }
    /**
     * Tells, whether to skip the rule or not.
     * @param mapping the mapping
     * @param ctx the context
     * @return true, if rule should be skipped, false otherwise
     */
    private boolean skipRule(RuleMappingElement mapping, DataQualityContext ctx) {
        // The rule is turned off os input source system is not supported
        return mapping.getRule().getRunCondition() == QualityRuleRunCondition.RUN_NEVER
           || !mapping.getRule().supports(ctx.getSourceSystem());
    }
    /**
     * Runs rule.
     * @param mapping the mapping
     * @param state the result
     * @param ctx the context
     * @param output the output
     */
    private void runRule(RuleMappingElement mapping, RuleExecutionResult state, DataQualityContext ctx, DataQualityOutput output) {

        MeasurementPoint.start();
        try {

            // Set variables for all cycles
            QualityRuleElement rule = mapping.getRule();
            CleanseFunctionElement function = rule.getCleanseFunction();
            Collection<CustomPropertyElement> properties = collectProperties(rule, function);

            // Create cleanse function context
            CleanseFunctionContext cfc = CleanseFunctionContext.builder()
                    .ruleName(rule.getName())
                    .functionName(function.getName())
                    .customProperties(properties)
                    .currentLocale(textService.getCurrentLocale())
                    .payload(ctx.getPayload())
                    .build();

            RuleExecutionPlan plan = new RuleExecutionPlan(mapping, state, output);
            for (RuleExecutionCycle cycle : plan) {

                // Prepare input
                Collection<CleanseFunctionInputParam> params = cycle.before();

                cfc.getInputParams().clear();
                cfc.input(params);

                // Check ability to execute. Skip on inability
                if (skipCycle(mapping, cfc)) {
                    state.skip();
                    continue;
                }

                // Execute the CF
                CleanseFunctionResult cfr = function.execute(cfc);

                // Process output and collect state
                cycle.after(cfc, cfr);
            }

            state.setValid(plan.isValid());
            state.setEnriched(plan.hadEnrichments());

        } finally {
            MeasurementPoint.stop();
        }
    }
    /*
     * Tells, whether to skip the rule cycle or not.
     *
     * @param mapping the rule to check
     * @param cfc current CF context
     * @return true, if rule should be skipped, false otherwise
     */
    private boolean skipCycle(RuleMappingElement mapping, CleanseFunctionContext cfc) {

        MeasurementPoint.start();
        try {

            QualityRuleElement rule = mapping.getRule();
            QualityRuleRunCondition runType = rule.getRunCondition() == null
                    ? QualityRuleRunCondition.RUN_ON_REQUIRED_PRESENT
                    : rule.getRunCondition();

            // 1. Check run always mode
            if (runType == QualityRuleRunCondition.RUN_ALWAYS) {
                return false;
            }

            // 2. Check required (or all) ports, exit silently, if some required ports are empty (original behaviour).
            for (PortElement inputPort : rule.getCleanseFunction().getInputPorts()) {

                CleanseFunctionParam param = cfc.getInputParam(inputPort.getName());
                if (Objects.nonNull(param) && !param.isEmpty()) {
                    continue;
                }

                if (runType == QualityRuleRunCondition.RUN_ON_ALL_PRESENT
                 || runType == QualityRuleRunCondition.RUN_ON_REQUIRED_PRESENT && inputPort.isRequired()) {
                    return true;
                }
            }

            return false;
        } finally {
            MeasurementPoint.stop();
        }
    }
    /*
     * Collects custom properties before rule execution.
     * @param rule the rule
     * @param function the function
     * @return collection
     */
    private Collection<CustomPropertyElement> collectProperties(QualityRuleElement rule, CleanseFunctionElement function) {

        if (CollectionUtils.isNotEmpty(rule.getCustomProperties())
         && CollectionUtils.isNotEmpty(function.getCustomProperties())) {
            return CollectionUtils.union(rule.getCustomProperties(), function.getCustomProperties());
        } else {
            return CollectionUtils.isNotEmpty(rule.getCustomProperties())
                   ? rule.getCustomProperties()
                   : function.getCustomProperties();
        }
    }
    /*
     * Handles cleanse function execution exception.
     *
     * @param ctx the context, being processed
     * @param rme the rule caused the exception
     * @param e the exception to handle
     */
    private void handleException(RuleExecutionResult result, RuleMappingElement rme, Exception e) {

        final String message = "DQ error: category [{}], "
                + "exception while executing cleanse function: '{}', "
                + "rule name: [{}], "
                + "exception message: [{}], "
                + "severity [{}].";

        final String functionName = rme.getRule().getCleanseFunction().getId();
        final String ruleName = rme.getRule().getName();

        LOGGER.warn(message,
                DQUtils.CATEGORY_SYSTEM,
                functionName,
                ruleName,
                e.getMessage(),
                SeverityIndicator.RED);

        final Object[] params = new Object[] {
                functionName,
                String.join("\n", ExceptionUtils.getRootCauseStackTrace(e))
        };

        result.add(DataQualityError.builder()
                .category(DQUtils.CATEGORY_SYSTEM)
                .message(MessageFormatter.arrayFormat("Exception while executing cleanse function '{}'\n Stacktrace: \n", params).getMessage())
                .severity(SeverityIndicator.RED.name())
                .ruleName(ruleName)
                .functionName(functionName)
                .build());
    }
    /*
     * @author Mikhail Mikhailov on Mar 10, 2021
     * Rule execution plan.
     */
    private class RuleExecutionPlan implements Iterable<RuleExecutionCycle> {
        /**
         * Validation state
         */
        private final boolean[] validations;
        /**
         * Enrichment state.
         */
        private final boolean[] enrichments;
        /**
         * The cycles.
         */
        private final RuleExecutionCycle[] cycles;
        /**
         * The output
         */
        private final DataQualityOutput output;
        /**
         * This rule mapping.
         */
        private final RuleMappingElement mapping;
        /**
         * The rule result (for accumulating of errors/failures/call paths).
         */
        private final RuleExecutionResult result;
        /**
         * Constructor.
         * @param mapping the rule mapping
         * @param result state collector
         * @param output global DQ output
         */
        public RuleExecutionPlan(@Nonnull RuleMappingElement mapping, @Nonnull RuleExecutionResult result, @Nonnull DataQualityOutput output) {
            super();
            this.output = output;
            this.cycles = mapping.isLocal() ? initLocal(mapping) : initGlobal();
            this.enrichments = new boolean[this.cycles.length];
            this.validations = new boolean[this.cycles.length];
            this.mapping = mapping;
            this.result = result;
        }
        /*
         * Global.
         */
        private RuleExecutionCycle[] initGlobal() {
            RuleExecutionCycle[] retval = new RuleExecutionCycle[1];
            retval[0] = new RuleExecutionCycle(0, this);
            return retval;
        }
        /*
         * Local.
         */
        private RuleExecutionCycle[] initLocal(RuleMappingElement mapping) {

            UPath localPath = mapping.getLocalPath();
            List<DataRecord> slot = output.getAsRecords(localPath.getQualifierKey());

            if (CollectionUtils.isEmpty(slot)) {
                return EMPTY_CYCLES_ARRAY;
            }

            List<DataRecord> collected;
            if (localPath.isRoot()) {
                collected = slot;
            } else {
                collected = new ArrayList<>();
                for (int i = 0;  i < slot.size(); i++) {

                    UPathResult selection = upathService.upathGet(localPath, slot.get(i), UPathApplicationMode.MODE_ALL);
                    for (Attribute attr : selection.getAttributes()) {
                        collected.addAll(((ComplexAttribute) attr).toCollection());
                    }
                }
            }

            RuleExecutionCycle[] retval = new RuleExecutionCycle[collected.size()];
            for (int i = 0; i < collected.size(); i++) {
                retval[i] = new RuleExecutionCycle(i, this, collected.get(i));
            }

            return retval;
        }
        /**
         * Reports overall validation state.
         * @return true, if all cycle validations are valid
         */
        public boolean isValid() {
            return BooleanUtils.and(validations);
        }
        /**
         * Reports if made some changes to records state (enrichments).
         * @return if made some changes to records state (enrichments)
         */
        public boolean hadEnrichments() {
            return BooleanUtils.or(enrichments);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Iterator<RuleExecutionCycle> iterator() {
            return new RuleExecutionIterator();
        }
        /**
         * @author Mikhail Mikhailov on Mar 10, 2021
         * Cycles iterator.
         */
        private class RuleExecutionIterator implements Iterator<RuleExecutionCycle> {
            /**
             * Current position.
             */
            private int position;
            /**
             * Constructor.
             */
            private RuleExecutionIterator() {
                super();
                position = 0;
            }
            /**
             * {@inheritDoc}
             */
            @Override
            public boolean hasNext() {
                return RuleExecutionPlan.this.cycles.length > 0 && position < RuleExecutionPlan.this.cycles.length;
            }
            /**
             * {@inheritDoc}
             */
            @Override
            public RuleExecutionCycle next() {

                if (RuleExecutionPlan.this.cycles.length == 0
                 || RuleExecutionPlan.this.cycles.length <= position) {
                    throw new NoSuchElementException("Access out of cycles bounds.");
                }

                return RuleExecutionPlan.this.cycles[position++];
            }
        }
    }
    /**
     * @author Mikhail Mikhailov on Mar 10, 2021
     * Cycle.
     */
    private class RuleExecutionCycle {
        /**
         * Local input.
         */
        private final List<DataRecord> local;
        /**
         * Global input
         */
        private final int ordinal;
        /**
         * The owning plan.
         */
        private final RuleExecutionPlan plan;
        /**
         * Constructor.
         * @param ordinal this cycle ordinal
         * @param plan the owning plan
         */
        public RuleExecutionCycle(int ordinal, RuleExecutionPlan plan, DataRecord local) {
            super();
            this.local = Collections.singletonList(local);
            this.ordinal = ordinal;
            this.plan = plan;
        }
        /**
         * Constructor.
         * @param ordinal this cycle ordinal
         */
        public RuleExecutionCycle(int ordinal, RuleExecutionPlan plan) {
            super();
            this.local = null;
            this.ordinal = ordinal;
            this.plan = plan;
        }
        /**
         * Before cleanse function run (collects input params).
         * @return params
         */
        public Collection<CleanseFunctionInputParam> before() {

            plan.validations[ordinal] = true;
            plan.enrichments[ordinal] = false;

            Collection<InputMappingElement> input = plan.mapping.getInputMappings();
            if (CollectionUtils.isEmpty(input)) {
                return Collections.emptyList();
            }

            List<CleanseFunctionInputParam> params = new ArrayList<>(input.size());
            for (InputMappingElement ime : input) {

                CleanseFunctionInputParam param;
                if (ime.isConstant()) {
                    param = ime.getConstant().getInput().toInput();
                } else {
                    param = collectParam(plan.mapping, ime, plan.mapping.isLocal()
                            ? local
                            : plan.output.getAsRecords(ime.getInputPath().getQualifierKey()));
                }

                if (Objects.nonNull(param)) {
                    params.add(param);
                }
            }

            return params;
        }
        /**
         * After cleanse function run (sets input params).
         * @param cfc the input
         * @param cfr the output
         */
        public void after(CleanseFunctionContext cfc, CleanseFunctionResult cfr) {

            plan.validations[ordinal] = processValidation(cfc, cfr);
            plan.enrichments[ordinal] = plan.validations[ordinal] && processEnrichment(cfr);

            plan.result.addErrors(cfr.getErrors());
            plan.result.addSpots(cfr.getSpots());
        }
        /*
         * Extracts indicator string from a singleton.
         */
        private<X> X processIndicator(CleanseFunctionResult cfr,
                boolean isPort,
                String portName,
                Function<String, X> extractor, X raiseValue, X defaultValue) {

            X result;
            if (isPort) {
                CleanseFunctionOutputParam param = cfr.getOutputParam(portName);
                SimpleAttribute<?> attribute = param != null && param.isSingleton() ? param.getSingleton() : null;
                String val = attribute != null && !attribute.isEmpty() ? attribute.castValue() : null;
                result = StringUtils.isBlank(val) ? defaultValue : extractor.apply(val);
            } else {
                result = raiseValue;
            }

            return Objects.isNull(result) ? defaultValue : result;
        }
        /*
         * Cycle validation.
         */
        private boolean processValidation(CleanseFunctionContext cfc, CleanseFunctionResult cfr) {

            QualityRuleElement rule = plan.mapping.getRule();
            if (!rule.isValidating()) {
                return true;
            }

            ValidatingRuleElement raise = rule.getValidating();
            CleanseFunctionOutputParam validate = cfr.getOutputParam(raise.getRaisePort());

            String category = null;
            String message = null;
            SeverityIndicator severity = null;
            int score = 0;

            boolean isValid = true;
            if (validate == null || validate.isEmpty()) {

                LOGGER.warn("Cleanse function [{}] didn't return required value for port [{}]!",
                        rule.getCleanseFunction().getName(), raise.getRaisePort());

                category = DQUtils.CATEGORY_SYSTEM;
                message = textService.getText("app.dq.missing.input.from.validating.rule", rule.getCleanseFunction().getName(), raise.getRaisePort());
                severity = SeverityIndicator.YELLOW;

                isValid = false;
            } else {

                SimpleAttribute<?> mark = validate.getSingleton();
                isValid = mark.getDataType() == SimpleAttribute.SimpleDataType.BOOLEAN && Boolean.TRUE.equals(mark.castValue());

                if (!isValid) {

                    // 1. Message
                    message = processIndicator(cfr, raise.hasMessagePort(), raise.getMessagePort(),
                            Function.identity(),
                            raise.getMessageText(),
                            "No value defined for DQ error message!");

                    // 2. Severity
                    severity = processIndicator(cfr, raise.hasSeverityPort(), raise.getSeverityPort(),
                            SeverityIndicator::valueOf,
                            raise.getSeverityIndicator(),
                            SeverityIndicator.YELLOW);

                    // 3. Yellow severity indicator
                    score = raise.getSeverityScore();

                    // 4. Category
                    category = processIndicator(cfr, raise.hasCategoryPort(), raise.getCategoryPort(),
                            Function.identity(),
                            raise.getCategoryText(),
                            DQUtils.CATEGORY_UNDEFINED);
                }
            }

            if (isValid) {
                return true;
            }

            plan.result.add(DataQualityError.builder()
                    .category(category)
                    .message(message)
                    .severity(severity)
                    .score(score)
                    .ruleName(rule.getName())
                    .functionName(rule.getCleanseFunction().getName())
                    .build());

            for (InputMappingElement m : plan.mapping.getInputMappings()) {
                plan.result.add(new DataQualityState(
                        m.getPortName(),
                        m.isConstant() ? "[CONSTANT]" : m.getInputPath().toUPath(), cfc.getInputParam(m.getPortName()).getAttributes()));
            }

            return false;
        }
        /*
         * Cycle enrichment.
         */
        private boolean processEnrichment(CleanseFunctionResult cfr) {

            QualityRuleElement rule = plan.mapping.getRule();
            if (!rule.isEnriching()) {
                return false;
            }

            boolean hadHits = false;
            for (OutputMappingElement ome : plan.mapping.getOutputMappings()) {

                if (rule.isValidating()
                && StringUtils.isNotBlank(rule.getValidating().getRaisePort())
                && StringUtils.equals(ome.getPortName(), rule.getValidating().getRaisePort())) {
                    continue;
                }

                CleanseFunctionOutputParam param;
                if (ome.isConstant()) {
                    param = ome.getConstant().getOutput().toOutput();
                } else {
                    param = cfr.getOutputParam(ome.getPortName());
                }

                boolean wasSet = applyParam(plan.mapping, ome, param, plan.mapping.isLocal()
                        ? local
                        : plan.output.getAsRecords(ome.getOutputPath().getQualifierKey()));

                hadHits = !hadHits ? wasSet : hadHits;
            }

            return hadHits;
        }
        /*
         * Collects attributes for an input param from several records.
         */
        @Nullable
        private CleanseFunctionInputParam collectParam(RuleMappingElement mapping, InputMappingElement ime, List<DataRecord> records) {

            if (CollectionUtils.isEmpty(records)) {
                return null;
            }

            List<Attribute> input = new ArrayList<>(4);
            List<UPathIncompletePath> incomplete =  new ArrayList<>(4);

            for (int i = 0; i < records.size(); i++) {

                UPathResult result = upathService.upathGet(ime.getInputPath(), records.get(i),
                        mapping.isLocal() ? UPathExecutionContext.SUB_TREE : UPathExecutionContext.FULL_TREE,
                        mapping.getRule()
                            .getCleanseFunction()
                            .getInputPortByName(ime.getPortName())
                            .getFilteringMode().toUPathMode());

                if (Objects.isNull(result) || result.isEmpty()) {
                    continue;
                }

                input.addAll(result.getAttributes());
                incomplete.addAll(result.getIncomplete());
            }

            return CleanseFunctionInputParam.of(ime.getPortName(), input, incomplete);
        }
        /*
         * Applies param to several filtered records.
         */
        private boolean applyParam(RuleMappingElement mapping, OutputMappingElement ome, CleanseFunctionOutputParam param, List<DataRecord> records) {

            // Non-null singleton is expected
            if (Objects.isNull(param) || !param.isSingleton()) {
                return false;
            }

            // Find final attribute name.
            // Report error and do nothing,
            // if last segment is not a collecting one.
            UPathElement tail = ome.getOutputPath().getTail();
            if (!tail.isCollecting()) {
                LOGGER.warn("A mapping on port [{}] for rule [{}] defines invalid output mapping (does not end with collecting segment).",
                        ome.getPortName(), mapping.getRule().getName());
                return false;
            }

            Attribute attribute = prepareParam(param.getSingleton(), tail.getElement());
            if (Objects.isNull(attribute)) {
                return false;
            }

            // Repackage with real name and apply
            boolean hadHits = false;
            for (int i = 0; i < records.size(); i++) {

                DataRecord record = records.get(i);
                boolean wasSet = upathService.upathSet(ome.getOutputPath(), record, attribute,
                        mapping.isLocal() ? UPathExecutionContext.SUB_TREE : UPathExecutionContext.FULL_TREE,
                        mapping.getRule()
                            .getCleanseFunction()
                            .getInputPortByName(ome.getPortName())
                            .getFilteringMode().toUPathMode());

                // Preserve previous application state
                hadHits = !hadHits ? wasSet : hadHits;
            }

            return hadHits;
        }
        /*
         * Applies (narrow and repackage) a single output param possibly narrowing it to a proper type.
         *
         * @param attribute the output
         * @param name
         * @return narrowed and repackaged attribute
         */
        private Attribute prepareParam(Attribute attribute, String name) {

            Attribute target = null;
            if (attribute.getAttributeType() == Attribute.AttributeType.SIMPLE) {
                SimpleAttribute<?> cast = attribute.narrow();
                target = AbstractSimpleAttribute.of(cast.getDataType(), name, cast.getValue());
            } else if (attribute.getAttributeType() == Attribute.AttributeType.ARRAY) {
                ArrayAttribute<?> cast = attribute.narrow();
                target = AbstractArrayAttribute.of(cast.getDataType(), name, cast.toArray());
            } else if (attribute.getAttributeType() == Attribute.AttributeType.COMPLEX) {
                ComplexAttribute cast = attribute.narrow();
                target = new ComplexAttributeImpl(name, cast.toCollection());
            }

            return target;
        }
    }
}
